/**
 * Copyright (C) 2011-2012 Luca Li Greci
 *
 * This file is part of pojo-creator
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.pojocreator;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;

import com.pojocreator.metamodel.MClass;
import com.pojocreator.metamodel.MetaModel;
import com.pojocreator.provider.DataProvider;
import com.pojocreator.provider.Provider;
import com.pojocreator.utility.ReflectionHelper;

/**
 * This class exposes the methods to create a POJO object with or without
 * setting the fields. <br>
 * <br>
 * <p>
 * For a brand new <b>empty</b> POJO, you can use the simplest signature:
 * 
 * <pre>
 * {@code
 *  one(Pojo.class)
 * }
 * </pre>
 * <p>
 * For a brand new POJO <b>with</b> properties, you can use one of the possible
 * signature:
 * 
 * <pre>
 * {@code
 * one(Pojo.class, with(property-name, property-value))
 * one(Pojo.class, with(property-name, property-value), with(property-name, property-value), etc..)
 * 
 * one(Pojo.class, with(property-name, data-provider))
 * one(Pojo.class, with(property-name, data-provider), with(property-name, data-provider), etc..)
 * }
 * </pre>
 * 
 * To instantiate more POJO <b>with</b> properties, you can use the following
 * signature:
 * 
 * <pre>
 * {@code
 * many(Pojo.class, numberOfPojo, with(property-name, data-provider))
 * many(Pojo.class, numberOfPojo, with(property-name, data-provider), with(property-name, data-provider), etc..)
 * }
 * </pre>
 * <p>
 * 
 * <b>N.B</b> To simplify the methods invocation use:
 * 
 * <pre>
 * {@code
 * import static com.pojocreator.Creator.many;
 * import static com.pojocreator.Creator.one;
 * import static com.pojocreator.Creator.with;
 * }
 * </pre>
 * <p>
 * 
 * @author Luca Li Greci
 */
public final class Creator < T > {
    private T bean;
    private static final ConcurrentMap < Class < ? >, MetaModel > META_CACHE = new ConcurrentHashMap < Class < ? >, MetaModel >();

    private Creator(Class < T > clazz) {
        bean = instanceOf(clazz);
        MetaModel metaModel = META_CACHE.get(clazz);
        if (metaModel == null) {
            META_CACHE.putIfAbsent(clazz, new MetaModel(clazz));
        }
    }

    /**
     * Create an instance of the required object based on the class type.
     * 
     * @param clazz Class type
     * @return a new instance without setting the object properties
     */
    public static < T > T one(Class < T > clazz) {
        return one(clazz, (Property < String >) null);
    }

    /**
     * Create an instance of the required object based on the class type and set
     * the object's field with a defined value
     * 
     * @param clazz Class type
     * @param attribute Attribute need to be set in the object
     * @return a new instance of the object with the property already been set.
     * @throws CreatorException in case of error during the POJO creation
     */
    public static < T > T one(Class < T > clazz, Property < ? > attribute) {
        Property < ? >[] attributes = null;
        if (attribute != null) {
            attributes = (Property < ? >[]) Array.newInstance(Property.class, 1);
            attributes[0] = attribute;
        }
        return one(clazz, attributes);
    }

    /**
     * Create an instance of the required object based on the class type and set
     * all fields with a defined value
     * 
     * @param clazz Class type
     * @param attributes Attributes need to be set in the object
     * @return a new instance of the object with the property already been set.
     * @throws CreatorException in case of error during the POJO creation
     */
    public static < T > T one(Class < T > clazz, Property < ? >... attributes) {
        Creator < T > object = new Creator < T >(clazz);
        setFields(object.bean, attributes);
        return object.bean;
    }

    /**
     * Create an instance of IterableCreator to instantiate many number of pojo
     * and iterate through them using a foreach loop.
     * 
     * @param clazz Class type to instantiate.
     * @param numberOfPojo Maximum number of instances
     * @param attributes Pojo's attribute
     * @return an {@link Iterable} to use in a foreach loop
     */
    public static < T > Iterable < T > many(Class < T > clazz, int numberOfPojo, Property < ? >... attributes) {
        return new IterableCreator < T >(clazz, numberOfPojo, attributes);
    }

    /**
     * Create a {@link Collection} of T elements using the attributes given. The
     * number of elements in the collection depends on the attributes, the first
     * step of this method is to to calculate all possible permutations.
     * <p>
     * For instance, a POJO which has 2 attributes:
     * <ul>
     * <li>attribute1 has M alternatives</li>
     * <li>attribute2 has N alternatives</li>
     * </ul>
     * The total number of POJOs in the collection will be M x N, follow an
     * example of Car that has attribute owner and brand.
     * <p>
     * Both attributes are set randomly from a data set:
     * <ul>
     * <li>brand can have values: Ferrari, BMW, Porche</li>
     * <li>owner can have values: Luca, Paul, Dan</li>
     * </ul>
     * <p>
     * The collection will have 9 elements.
     * 
     * <pre>
     * Collection &lt; Car &gt; cars = many(Car.class, with(&quot;brand&quot;, from(&quot;Ferrari&quot;, &quot;BMW&quot;, &quot;Porche&quot;)), with(&quot;owner&quot;, from(&quot;Luca&quot;, &quot;Paul&quot;, &quot;Dan&quot;)));
     * </pre>
     * 
     * @param clazz Class type to instantiate.
     * @param attributes Pojo's attribute
     * @return a {@link Collection} of element T with the property set
     */
    public static < T > Collection < T > many(Class < T > clazz, Property < ? >... attributes) {
        int numberOfPojos = calculateNumberOfPermutation(attributes);
        Collection < T > out = new ArrayList < T >(numberOfPojos);
        for (int i = 0; i < numberOfPojos; ++i) {
            out.add(one(clazz, attributes));
        }
        return out;
    }

    /**
     * Create a new {@link Property} object for a specific object field's name
     * with a value.
     * 
     * @param property Name of the object's field we want to set
     * @param value Value we want to set
     * @return an new Property
     */
    public static < S > Property < S > with(String property, S value) {
        return new Property < S >(property, value);
    }

    public static < S > Property < S > with(S value) {
        return new DynamicProperty < S >(value);
    }

    /**
     * Create a new {@link DataProvider} to extract a random element S among the
     * values passed in as argument.
     * <p>
     * The {@link DataProvider} can be used in method like
     * {@link Creator#with(String, Object) with}. For instance, to create a new
     * Car with the brand set from <b>one</b> of the elements in the array you
     * can use:
     * <p>
     * 
     * <pre>
     * Car car = one(Car.class, with(&quot;brand&quot;, from(&quot;Ferrari&quot;, &quot;BMW&quot;, &quot;Porche&quot;)));
     * </pre>
     * 
     * @param values array of elements to use as dataset.
     * @param <S> element datatype
     * @return a new {@link DataProvider}
     */
    public static < S > DataProvider < S > from(S... values) {
        return Provider.randomFrom(values);
    }

    /**
     * Create an instance of the generic type T
     * 
     * @param clazz Class type
     * @return an instance of the generic type T
     * @throws CreatorException in case of error during the POJO creation
     */
    private T instanceOf(Class < T > clazz) {
        if (clazz == null) {
            throw new CreatorException("Class definition cannot be null. Impossible to create null pojo");
        }
        try {
            return clazz.newInstance();
        } catch (InstantiationException e) {
            throw new CreatorException("Impossible to instantiate class " + clazz.getName() + ".", e);
        } catch (IllegalAccessException e) {
            throw new CreatorException("Impossible to instantiate class " + clazz.getName() + ". Cannot access the public default constructor", e);
        }
    }

    /**
     * Set the object values for a given list of attributes
     * 
     * @param object instance of T
     * @param attributes attribute's property
     * @throws CreatorException in case of error during the the property setting
     */
    private static < T, S > void setFields(T object, Property < ? >... attributes) {
        if (attributes == null) {
            return;
        }
        MetaModel metaModel = META_CACHE.get(object.getClass());

        for (int i = 0; i < attributes.length; i++) {
            Property < ? > attribute = attributes[i];
            if (attribute == null) {
                return;
            }
            setField(object, attribute);
            linker(object, attribute, metaModel);
        }
    }

    /**
     * Set the object's value for a given attribute
     * 
     * @param object instance of T
     * @param attribute attribute to set.
     * @param <T> Type of the object.
     * @param <S> Type of the value in the Property. 
     * @throws CreatorException in case of error during the property setting
     */
    private static < T, S > void setField(T object, Property < S > attribute) {
        String propertyName = propertyName(object, attribute);
        if (StringUtils.isBlank(propertyName)) {
            return;
        }
        S propertyValue = attribute.getValue();
        try {
            Field field = object.getClass().getDeclaredField(propertyName);
            ReflectionHelper.set(field, object, propertyValue);
        } catch (SecurityException e) {
            throw new CreatorException("Impossible to set field " + propertyName + " due to security problem", e);
        } catch (NoSuchFieldException e) {
            throw new CreatorException("Missing field " + propertyName + " in the object " + object.getClass(), e);
        } catch (IllegalArgumentException e) {
            throw new CreatorException("Attribute value cannot be set for field " + propertyName, e);
        } catch (IllegalAccessException e) {
            throw new CreatorException("Cannot access object to set field " + propertyName, e);
        }
    }

    /**
     * Return the property name for a given attribute in the object.
     * 
     * @param object instance of T
     * @param attribute attribute to set.
     * @param <T> Type of the object.
     * @param <S> Type of the value in the Property. 
     * @return the property name for a given attribute in the object T.
     */
    private static < T, S > String propertyName(T object, Property < S > attribute) {
        String propertyName = attribute.getName();
        if (attribute instanceof DynamicProperty < ? >) {
            propertyName = propertyNameByDestinationType(object, attribute.getValue());
        }
        return propertyName;
    }

    /**
     * Try to identify the property name using the destination type.
     * <p>
     * If the value data type can be set in more than one property an {@link AmbiguousPropertyTypeException} 
     * is thrown.
     * 
     * @param object instance of T
     * @param value value to set
     * @return
     */
    private static < T, S > String propertyNameByDestinationType(T object, S value) {
        MetaModel metaModel = META_CACHE.get(object.getClass());
        List < MClass > list = metaModel.model(value.getClass());
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        if (list.size() > 1) {
            throw new AmbiguousPropertyTypeException("Impossible to set the value " + value + " using only its data type " + value.getClass());
        }
        return list.get(0).getName();
    }

    /**
     * For a given object, access the meta model information to identify the
     * relationship with other objects. For instance, the traversal direction in
     * a one to many relationship that can be accessed both ways, e.g. a writer
     * has many books, and also a book as a writer, the framework has to keep
     * track of the link and set the necessary property field accordingly.
     * 
     * @param object current object under construction.
     * @param attribute current attribute to set.
     * @param metaModel meta model that describes the links.
     */
    private static < T, S > void linker(T object, Property < S > attribute, MetaModel metaModel) {
        MClass model = metaModel.model(attribute.getName());
        if (model == null) {
            return;
        }
        List < MClass > models = metaModel.models(model.getType());
        if (CollectionUtils.isEmpty(models)) {
            return;
        }

        for (MClass mc : models) {
            if (object.getClass().equals(mc.getType())) {
                Object field = attribute.getValue();
                Property < T > p = new Property < T >(mc.getName(), object);
                setField(field, p);
            }
        }
    }

    /**
     * Calculate the number of permutation for a given set of attributes.
     * 
     * @param attributes Attributes to set.
     * @return the number of possible permutation for the given attributes.
     */
    private static int calculateNumberOfPermutation(final Property < ? >... attributes) {
        int numberOfPojos = 1;
        for (Property < ? > p : attributes) {
            Object value = p.getValue();
            if (value instanceof DataProvider < ? >) {
                DataProvider < ? > t = (DataProvider < ? >) value;
                numberOfPojos *= t.size();
            } else {
                numberOfPojos *= 1;
            }
        }
        return numberOfPojos;
    }

    /**
     * This inner class is to allow the user to create many POJO representations
     * with a using a convenient foreach syntax.
     * <p>
     * For example:
     * 
     * <pre>
     * {@code
     * for (Car car : many(Car.class, 3, with("brand", "Ferrari"))) {
     * }
     * </pre>
     * 
     * @param <T> Generic datatype for the object
     */
    private static class IterableCreator < T > implements Iterable < T >, Iterator < T > {
        private int numberOfElements;
        private int created;
        private Property < ? >[] attributes;
        private Class < T > clazz;

        IterableCreator(Class < T > clazz, int numberOfElements, Property < ? >... attributes) {
            this.numberOfElements = numberOfElements;
            this.attributes = attributes;
            this.clazz = clazz;
        }

        @Override
        public boolean hasNext() {
            if (created < numberOfElements) {
                return true;
            }
            return false;
        }

        @Override
        public T next() {
            if (created == numberOfElements) {
                throw new NoSuchElementException("The  max number of elements has been reached");
            }
            T pojo = one(clazz, attributes);
            created++;
            return pojo;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Remove is not supported");
        }

        @Override
        public Iterator < T > iterator() {
            return this;
        }
    }

}
